"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

require("core-js/modules/es.array.concat");

require("core-js/modules/es.array.iterator");

require("core-js/modules/es.array.slice");

require("core-js/modules/es.array-buffer.constructor");

require("core-js/modules/es.array-buffer.slice");

require("core-js/modules/es.object.assign");

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.typed-array.uint8-array");

require("core-js/modules/es.typed-array.copy-within");

require("core-js/modules/es.typed-array.every");

require("core-js/modules/es.typed-array.fill");

require("core-js/modules/es.typed-array.filter");

require("core-js/modules/es.typed-array.find");

require("core-js/modules/es.typed-array.find-index");

require("core-js/modules/es.typed-array.for-each");

require("core-js/modules/es.typed-array.includes");

require("core-js/modules/es.typed-array.index-of");

require("core-js/modules/es.typed-array.iterator");

require("core-js/modules/es.typed-array.join");

require("core-js/modules/es.typed-array.last-index-of");

require("core-js/modules/es.typed-array.map");

require("core-js/modules/es.typed-array.reduce");

require("core-js/modules/es.typed-array.reduce-right");

require("core-js/modules/es.typed-array.reverse");

require("core-js/modules/es.typed-array.set");

require("core-js/modules/es.typed-array.slice");

require("core-js/modules/es.typed-array.some");

require("core-js/modules/es.typed-array.sort");

require("core-js/modules/es.typed-array.subarray");

require("core-js/modules/es.typed-array.to-locale-string");

require("core-js/modules/es.typed-array.to-string");

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

var _logger = _interopRequireDefault(require("./utils/logger.js"));

var _speedSampler = _interopRequireDefault(require("./speed-sampler.js"));

var _loader = require("./loader.js");

var _fetchStreamLoader = _interopRequireDefault(require("./fetch-stream-loader.js"));

var _xhrMozChunkedLoader = _interopRequireDefault(require("./xhr-moz-chunked-loader.js"));

var _xhrMsstreamLoader = _interopRequireDefault(require("./xhr-msstream-loader.js"));

var _xhrRangeLoader = _interopRequireDefault(require("./xhr-range-loader.js"));

var _websocketLoader = _interopRequireDefault(require("./websocket-loader.js"));

var _rangeSeekHandler = _interopRequireDefault(require("./range-seek-handler.js"));

var _paramSeekHandler = _interopRequireDefault(require("./param-seek-handler.js"));

var _exception = require("./utils/exception.js");

/*
 * Copyright (C) 2016 Bilibili. All Rights Reserved.
 *
 * @author zheng qian <xqq@xqq.im>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * DataSource: {
 *     url: string,
 *     filesize: number,
 *     cors: boolean,
 *     withCredentials: boolean
 * }
 * 
 */
// Manage IO Loaders
var IOController = /*#__PURE__*/function () {
  function IOController(dataSource, config, extraData) {
    (0, _classCallCheck2.default)(this, IOController);
    this.TAG = 'IOController';
    this._config = config;
    this._extraData = extraData;
    this._stashInitialSize = 1024 * 384; // default initial size: 384KB

    if (config.stashInitialSize != undefined && config.stashInitialSize > 0) {
      // apply from config
      this._stashInitialSize = config.stashInitialSize;
    }

    this._stashUsed = 0;
    this._stashSize = this._stashInitialSize;
    this._bufferSize = 1024 * 384; // 1024 * 1024 * 3;  // initial size: 3MB

    this._stashBuffer = new ArrayBuffer(this._bufferSize);
    this._stashByteStart = 0;
    this._enableStash = true;

    if (config.enableStashBuffer === false) {
      this._enableStash = false;
    }

    this._loader = null;
    this._loaderClass = null;
    this._seekHandler = null;
    this._dataSource = dataSource;
    this._isWebSocketURL = /wss?:\/\/(.+?)/.test(dataSource.url);
    this._refTotalLength = dataSource.filesize ? dataSource.filesize : null;
    this._totalLength = this._refTotalLength;
    this._fullRequestFlag = false;
    this._currentRange = null;
    this._redirectedURL = null;
    this._speedNormalized = 0;
    this._speedSampler = new _speedSampler.default();
    this._speedNormalizeList = [64, 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096];
    this._isEarlyEofReconnecting = false;
    this._paused = false;
    this._resumeFrom = 0;
    this._onDataArrival = null;
    this._onSeeked = null;
    this._onError = null;
    this._onComplete = null;
    this._onRedirect = null;
    this._onRecoveredEarlyEof = null;

    this._selectSeekHandler();

    this._selectLoader();

    this._createLoader();
  }

  (0, _createClass2.default)(IOController, [{
    key: "destroy",
    value: function destroy() {
      if (this._loader.isWorking()) {
        this._loader.abort();
      }

      this._loader.destroy();

      this._loader = null;
      this._loaderClass = null;
      this._dataSource = null;
      this._stashBuffer = null;
      this._stashUsed = this._stashSize = this._bufferSize = this._stashByteStart = 0;
      this._currentRange = null;
      this._speedSampler = null;
      this._isEarlyEofReconnecting = false;
      this._onDataArrival = null;
      this._onSeeked = null;
      this._onError = null;
      this._onComplete = null;
      this._onRedirect = null;
      this._onRecoveredEarlyEof = null;
      this._extraData = null;
    }
  }, {
    key: "isWorking",
    value: function isWorking() {
      return this._loader && this._loader.isWorking() && !this._paused;
    }
  }, {
    key: "isPaused",
    value: function isPaused() {
      return this._paused;
    }
  }, {
    key: "_selectSeekHandler",
    value: function _selectSeekHandler() {
      var config = this._config;

      if (config.seekType === 'range') {
        this._seekHandler = new _rangeSeekHandler.default(this._config.rangeLoadZeroStart);
      } else if (config.seekType === 'param') {
        var paramStart = config.seekParamStart || 'bstart';
        var paramEnd = config.seekParamEnd || 'bend';
        this._seekHandler = new _paramSeekHandler.default(paramStart, paramEnd);
      } else if (config.seekType === 'custom') {
        if (typeof config.customSeekHandler !== 'function') {
          throw new _exception.InvalidArgumentException('Custom seekType specified in config but invalid customSeekHandler!');
        }

        this._seekHandler = new config.customSeekHandler();
      } else {
        throw new _exception.InvalidArgumentException("Invalid seekType in config: ".concat(config.seekType));
      }
    }
  }, {
    key: "_selectLoader",
    value: function _selectLoader() {
      if (this._config.customLoader != null) {
        this._loaderClass = this._config.customLoader;
      } else if (this._isWebSocketURL) {
        this._loaderClass = _websocketLoader.default;
      } else if (this._config.loaderType == 'fetch' && _fetchStreamLoader.default.isSupported()) {
        this._loaderClass = _fetchStreamLoader.default;
      } else if (this._config.loaderType == 'fetch' && _xhrMozChunkedLoader.default.isSupported()) {
        this._loaderClass = _xhrMozChunkedLoader.default;
      } else if (_xhrRangeLoader.default.isSupported()) {
        this._loaderClass = _xhrRangeLoader.default;
      } else {
        throw new _exception.RuntimeException('Your browser doesn\'t support xhr with arraybuffer responseType!');
      }
    }
  }, {
    key: "_createLoader",
    value: function _createLoader() {
      this._loader = new this._loaderClass(this._seekHandler, this._config);

      if (this._loader.needStashBuffer === false) {
        this._enableStash = false;
      }

      this._loader.onContentLengthKnown = this._onContentLengthKnown.bind(this);
      this._loader.onURLRedirect = this._onURLRedirect.bind(this);
      this._loader.onDataArrival = this._onLoaderChunkArrival.bind(this);
      this._loader.onComplete = this._onLoaderComplete.bind(this);
      this._loader.onError = this._onLoaderError.bind(this);
    }
  }, {
    key: "open",
    value: function open(optionalFrom) {
      this._currentRange = {
        from: 0,
        to: -1
      };

      if (optionalFrom) {
        this._currentRange.from = optionalFrom;
      }

      this._speedSampler.reset();

      if (!optionalFrom) {
        this._fullRequestFlag = true;
      }

      this._loader.open(this._dataSource, Object.assign({}, this._currentRange));
    }
  }, {
    key: "abort",
    value: function abort() {
      this._loader.abort();

      if (this._paused) {
        this._paused = false;
        this._resumeFrom = 0;
      }
    }
  }, {
    key: "pause",
    value: function pause() {
      if (this.isWorking()) {
        this._loader.abort();

        if (this._stashUsed !== 0) {
          this._resumeFrom = this._stashByteStart;
          this._currentRange.to = this._stashByteStart - 1;
        } else {
          this._resumeFrom = this._currentRange.to + 1;
        }

        this._stashUsed = 0;
        this._stashByteStart = 0;
        this._paused = true;
      }
    }
  }, {
    key: "resume",
    value: function resume() {
      if (this._paused) {
        this._paused = false;
        var bytes = this._resumeFrom;
        this._resumeFrom = 0;

        this._internalSeek(bytes, true);
      }
    }
  }, {
    key: "seek",
    value: function seek(bytes) {
      this._paused = false;
      this._stashUsed = 0;
      this._stashByteStart = 0;

      this._internalSeek(bytes, true);
    }
    /**
     * When seeking request is from media seeking, unconsumed stash data should be dropped
     * However, stash data shouldn't be dropped if seeking requested from http reconnection
     *
     * @dropUnconsumed: Ignore and discard all unconsumed data in stash buffer
     */

  }, {
    key: "_internalSeek",
    value: function _internalSeek(bytes, dropUnconsumed) {
      if (this._loader.isWorking()) {
        this._loader.abort();
      } // dispatch & flush stash buffer before seek


      this._flushStashBuffer(dropUnconsumed);

      this._loader.destroy();

      this._loader = null;
      var requestRange = {
        from: bytes,
        to: -1
      };
      this._currentRange = {
        from: requestRange.from,
        to: -1
      };

      this._speedSampler.reset();

      this._stashSize = this._stashInitialSize;

      this._createLoader();

      this._loader.open(this._dataSource, requestRange);

      if (this._onSeeked) {
        this._onSeeked();
      }
    }
  }, {
    key: "updateUrl",
    value: function updateUrl(url) {
      if (!url || typeof url !== 'string' || url.length === 0) {
        throw new _exception.InvalidArgumentException('Url must be a non-empty string!');
      }

      this._dataSource.url = url; // TODO: replace with new url
    }
  }, {
    key: "_expandBuffer",
    value: function _expandBuffer(expectedBytes) {
      var bufferNewSize = this._stashSize;

      while (bufferNewSize + 1024 * 1024 * 1 < expectedBytes) {
        bufferNewSize *= 2;
      }

      bufferNewSize += 1024 * 1024 * 1; // bufferSize = stashSize + 1MB

      if (bufferNewSize === this._bufferSize) {
        return;
      }

      var newBuffer = new ArrayBuffer(bufferNewSize);

      if (this._stashUsed > 0) {
        // copy existing data into new buffer
        var stashOldArray = new Uint8Array(this._stashBuffer, 0, this._stashUsed);
        var stashNewArray = new Uint8Array(newBuffer, 0, bufferNewSize);
        stashNewArray.set(stashOldArray, 0);
      }

      this._stashBuffer = newBuffer;
      this._bufferSize = bufferNewSize;
    }
  }, {
    key: "_normalizeSpeed",
    value: function _normalizeSpeed(input) {
      var list = this._speedNormalizeList;
      var last = list.length - 1;
      var mid = 0;
      var lbound = 0;
      var ubound = last;

      if (input < list[0]) {
        return list[0];
      } // binary search


      while (lbound <= ubound) {
        mid = lbound + Math.floor((ubound - lbound) / 2);

        if (mid === last || input >= list[mid] && input < list[mid + 1]) {
          return list[mid];
        } else if (list[mid] < input) {
          lbound = mid + 1;
        } else {
          ubound = mid - 1;
        }
      }
    }
  }, {
    key: "_adjustStashSize",
    value: function _adjustStashSize(normalized) {
      var stashSizeKB = 0;

      if (this._config.isLive) {
        // live stream: always use single normalized speed for size of stashSizeKB
        stashSizeKB = normalized;
      } else {
        if (normalized < 512) {
          stashSizeKB = normalized;
        } else if (normalized >= 512 && normalized <= 1024) {
          stashSizeKB = Math.floor(normalized * 1.5);
        } else {
          stashSizeKB = normalized * 2;
        }
      }

      if (stashSizeKB > 8192) {
        stashSizeKB = 8192;
      }

      var bufferSize = stashSizeKB * 1024 + 1024 * 1024 * 1; // stashSize + 1MB

      if (this._bufferSize < bufferSize) {
        this._expandBuffer(bufferSize);
      }

      this._stashSize = stashSizeKB * 1024;
    }
  }, {
    key: "_dispatchChunks",
    value: function _dispatchChunks(chunks, byteStart) {
      this._currentRange.to = byteStart + chunks.byteLength - 1;
      return this._onDataArrival(chunks, byteStart);
    }
  }, {
    key: "_onURLRedirect",
    value: function _onURLRedirect(redirectedURL) {
      this._redirectedURL = redirectedURL;

      if (this._onRedirect) {
        this._onRedirect(redirectedURL);
      }
    }
  }, {
    key: "_onContentLengthKnown",
    value: function _onContentLengthKnown(contentLength) {
      if (contentLength && this._fullRequestFlag) {
        this._totalLength = contentLength;
        this._fullRequestFlag = false;
      }
    }
  }, {
    key: "_onLoaderChunkArrival",
    value: function _onLoaderChunkArrival(chunk, byteStart, receivedLength) {
      if (!this._onDataArrival) {
        throw new _exception.IllegalStateException('IOController: No existing consumer (onDataArrival) callback!');
      }

      if (this._paused) {
        return;
      }

      if (this._isEarlyEofReconnecting) {
        // Auto-reconnect for EarlyEof succeed, notify to upper-layer by callback
        this._isEarlyEofReconnecting = false;

        if (this._onRecoveredEarlyEof) {
          this._onRecoveredEarlyEof();
        }
      }

      this._speedSampler.addBytes(chunk.byteLength); // adjust stash buffer size according to network speed dynamically


      var KBps = this._speedSampler.lastSecondKBps;

      if (KBps !== 0) {
        var normalized = this._normalizeSpeed(KBps);

        if (this._speedNormalized !== normalized) {
          this._speedNormalized = normalized;

          this._adjustStashSize(normalized);
        }
      }

      if (!this._enableStash) {
        // disable stash
        if (this._stashUsed === 0) {
          // dispatch chunk directly to consumer;
          // check ret value (consumed bytes) and stash unconsumed to stashBuffer
          var consumed = this._dispatchChunks(chunk, byteStart);

          if (consumed < chunk.byteLength) {
            // unconsumed data remain.
            var remain = chunk.byteLength - consumed;

            if (remain > this._bufferSize) {
              this._expandBuffer(remain);
            }

            var stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);
            stashArray.set(new Uint8Array(chunk, consumed), 0);
            this._stashUsed += remain;
            this._stashByteStart = byteStart + consumed;
          }
        } else {
          // else: Merge chunk into stashBuffer, and dispatch stashBuffer to consumer.
          if (this._stashUsed + chunk.byteLength > this._bufferSize) {
            this._expandBuffer(this._stashUsed + chunk.byteLength);
          }

          var _stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);

          _stashArray.set(new Uint8Array(chunk), this._stashUsed);

          this._stashUsed += chunk.byteLength;

          var _consumed = this._dispatchChunks(this._stashBuffer.slice(0, this._stashUsed), this._stashByteStart);

          if (_consumed < this._stashUsed && _consumed > 0) {
            // unconsumed data remain
            var remainArray = new Uint8Array(this._stashBuffer, _consumed);

            _stashArray.set(remainArray, 0);
          }

          this._stashUsed -= _consumed;
          this._stashByteStart += _consumed;
        }
      } else {
        // enable stash
        if (this._stashUsed === 0 && this._stashByteStart === 0) {
          // seeked? or init chunk?
          // This is the first chunk after seek action
          this._stashByteStart = byteStart;
        }

        if (this._stashUsed + chunk.byteLength <= this._stashSize) {
          // just stash
          var _stashArray2 = new Uint8Array(this._stashBuffer, 0, this._stashSize);

          _stashArray2.set(new Uint8Array(chunk), this._stashUsed);

          this._stashUsed += chunk.byteLength;
        } else {
          // stashUsed + chunkSize > stashSize, size limit exceeded
          var _stashArray3 = new Uint8Array(this._stashBuffer, 0, this._bufferSize);

          if (this._stashUsed > 0) {
            // There're stash datas in buffer
            // dispatch the whole stashBuffer, and stash remain data
            // then append chunk to stashBuffer (stash)
            var buffer = this._stashBuffer.slice(0, this._stashUsed);

            var _consumed2 = this._dispatchChunks(buffer, this._stashByteStart);

            if (_consumed2 < buffer.byteLength) {
              if (_consumed2 > 0) {
                var _remainArray = new Uint8Array(buffer, _consumed2);

                _stashArray3.set(_remainArray, 0);

                this._stashUsed = _remainArray.byteLength;
                this._stashByteStart += _consumed2;
              }
            } else {
              this._stashUsed = 0;
              this._stashByteStart += _consumed2;
            }

            if (this._stashUsed + chunk.byteLength > this._bufferSize) {
              this._expandBuffer(this._stashUsed + chunk.byteLength);

              _stashArray3 = new Uint8Array(this._stashBuffer, 0, this._bufferSize);
            }

            _stashArray3.set(new Uint8Array(chunk), this._stashUsed);

            this._stashUsed += chunk.byteLength;
          } else {
            // stash buffer empty, but chunkSize > stashSize (oh, holy shit)
            // dispatch chunk directly and stash remain data
            var _consumed3 = this._dispatchChunks(chunk, byteStart);

            if (_consumed3 < chunk.byteLength) {
              var _remain = chunk.byteLength - _consumed3;

              if (_remain > this._bufferSize) {
                this._expandBuffer(_remain);

                _stashArray3 = new Uint8Array(this._stashBuffer, 0, this._bufferSize);
              }

              _stashArray3.set(new Uint8Array(chunk, _consumed3), 0);

              this._stashUsed += _remain;
              this._stashByteStart = byteStart + _consumed3;
            }
          }
        }
      }
    }
  }, {
    key: "_flushStashBuffer",
    value: function _flushStashBuffer(dropUnconsumed) {
      if (this._stashUsed > 0) {
        var buffer = this._stashBuffer.slice(0, this._stashUsed);

        var consumed = this._dispatchChunks(buffer, this._stashByteStart);

        var remain = buffer.byteLength - consumed;

        if (consumed < buffer.byteLength) {
          if (dropUnconsumed) {
            _logger.default.w(this.TAG, "".concat(remain, " bytes unconsumed data remain when flush buffer, dropped"));
          } else {
            if (consumed > 0) {
              var stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);
              var remainArray = new Uint8Array(buffer, consumed);
              stashArray.set(remainArray, 0);
              this._stashUsed = remainArray.byteLength;
              this._stashByteStart += consumed;
            }

            return 0;
          }
        }

        this._stashUsed = 0;
        this._stashByteStart = 0;
        return remain;
      }

      return 0;
    }
  }, {
    key: "_onLoaderComplete",
    value: function _onLoaderComplete(from, to) {
      // Force-flush stash buffer, and drop unconsumed data
      this._flushStashBuffer(true);

      if (this._onComplete) {
        this._onComplete(this._extraData);
      }
    }
  }, {
    key: "_onLoaderError",
    value: function _onLoaderError(type, data) {
      _logger.default.e(this.TAG, "Loader error, code = ".concat(data.code, ", msg = ").concat(data.msg));

      this._flushStashBuffer(false);

      if (this._isEarlyEofReconnecting) {
        // Auto-reconnect for EarlyEof failed, throw UnrecoverableEarlyEof error to upper-layer
        this._isEarlyEofReconnecting = false;
        type = _loader.LoaderErrors.UNRECOVERABLE_EARLY_EOF;
      }

      switch (type) {
        case _loader.LoaderErrors.EARLY_EOF:
          {
            if (!this._config.isLive) {
              // Do internal http reconnect if not live stream
              if (this._totalLength) {
                var nextFrom = this._currentRange.to + 1;

                if (nextFrom < this._totalLength) {
                  _logger.default.w(this.TAG, 'Connection lost, trying reconnect...');

                  this._isEarlyEofReconnecting = true;

                  this._internalSeek(nextFrom, false);
                }

                return;
              } // else: We don't know totalLength, throw UnrecoverableEarlyEof

            } // live stream: throw UnrecoverableEarlyEof error to upper-layer


            type = _loader.LoaderErrors.UNRECOVERABLE_EARLY_EOF;
            break;
          }

        case _loader.LoaderErrors.UNRECOVERABLE_EARLY_EOF:
        case _loader.LoaderErrors.CONNECTING_TIMEOUT:
        case _loader.LoaderErrors.HTTP_STATUS_CODE_INVALID:
        case _loader.LoaderErrors.EXCEPTION:
          break;
      }

      if (this._onError) {
        this._onError(type, data);
      } else {
        throw new _exception.RuntimeException('IOException: ' + data.msg);
      }
    }
  }, {
    key: "status",
    get: function get() {
      return this._loader.status;
    }
  }, {
    key: "extraData",
    get: function get() {
      return this._extraData;
    },
    set: function set(data) {
      this._extraData = data;
    } // prototype: function onDataArrival(chunks: ArrayBuffer, byteStart: number): number

  }, {
    key: "onDataArrival",
    get: function get() {
      return this._onDataArrival;
    },
    set: function set(callback) {
      this._onDataArrival = callback;
    }
  }, {
    key: "onSeeked",
    get: function get() {
      return this._onSeeked;
    },
    set: function set(callback) {
      this._onSeeked = callback;
    } // prototype: function onError(type: number, info: {code: number, msg: string}): void

  }, {
    key: "onError",
    get: function get() {
      return this._onError;
    },
    set: function set(callback) {
      this._onError = callback;
    }
  }, {
    key: "onComplete",
    get: function get() {
      return this._onComplete;
    },
    set: function set(callback) {
      this._onComplete = callback;
    }
  }, {
    key: "onRedirect",
    get: function get() {
      return this._onRedirect;
    },
    set: function set(callback) {
      this._onRedirect = callback;
    }
  }, {
    key: "onRecoveredEarlyEof",
    get: function get() {
      return this._onRecoveredEarlyEof;
    },
    set: function set(callback) {
      this._onRecoveredEarlyEof = callback;
    }
  }, {
    key: "currentURL",
    get: function get() {
      return this._dataSource.url;
    }
  }, {
    key: "hasRedirect",
    get: function get() {
      return this._redirectedURL != null || this._dataSource.redirectedURL != undefined;
    }
  }, {
    key: "currentRedirectedURL",
    get: function get() {
      return this._redirectedURL || this._dataSource.redirectedURL;
    } // in KB/s

  }, {
    key: "currentSpeed",
    get: function get() {
      if (this._loaderClass === _xhrRangeLoader.default) {
        // SpeedSampler is inaccuracy if loader is RangeLoader
        return this._loader.currentSpeed;
      }

      return this._speedSampler.lastSecondKBps;
    }
  }, {
    key: "loaderType",
    get: function get() {
      return this._loader.type;
    }
  }]);
  return IOController;
}();

var _default = IOController;
exports.default = _default;